﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Linq;
using MongoDB.Driver.Builders;
using MongoDB.Bson.Serialization;

using log4net;
using log4net.Repository.Hierarchy;

using Ingenious.Graph;
using Ingenious.Core;

namespace Ingenious.GraphStore
{
    /// <summary>
    /// Weak reference cache of Mongo database references
    /// </summary>
    internal class MongoDatabaseCache : IDisposable
    {
        #region Logging
        private static readonly ILog log = LogManager.GetLogger(typeof(MongoDatabaseCache));
        private static readonly bool loggingEnabled = log.IsDebugEnabled || log.IsErrorEnabled || log.IsFatalEnabled || log.IsInfoEnabled || log.IsWarnEnabled;
        #endregion

        #region Constants
        private const string TriplesCollectionName = "Triples";
        #endregion

        private readonly MongoServer m_server;
        private readonly Dictionary<string, WeakReference> m_databaseCollectionCache = new Dictionary<string, WeakReference>();

        private volatile bool m_disposed;

        #region Object Lifetime
        public MongoDatabaseCache(MongoServer server, IEnumerable<string> graphNames)
        {
            if( server == null )
            {
                throw new ArgumentNullException("server");
            }

            if( graphNames == null )
            {
                throw new ArgumentNullException("graphNames");
            }

            foreach( string graphName in graphNames )
            {
                // Initialise first with null weak reference
                m_databaseCollectionCache.Add(graphName, new WeakReference(null));
            }

            m_server = server;
        }

        public void Dispose()
        {
            if(!m_disposed)
            {
                m_databaseCollectionCache.Clear();
                m_disposed = true;
            }
        }
        #endregion

        public bool TryGetMongoTriplesCollection(string graphName, out MongoCollection graphTriplesCollection)
        {
            if( m_disposed )
            {
                throw new ObjectDisposedException("MongoDatabaseCache");
            }

            WeakReference collectionWeakReference = null;
            graphTriplesCollection = null;

            if (!m_databaseCollectionCache.TryGetValue(graphName, out collectionWeakReference))
            {
                // See if the database has been added since first connection
                bool success = false;

                try
                {
                    m_server.Ping();
                    success = true;
                }
                catch(MongoException ex)
                {
                    if( loggingEnabled ) log.Error("Could not ping Mongo database server", ex);
                }

                if (success)
                {
                    success = false;

                    MongoDatabase graphDatabase = null;

                    try
                    {
                        // Creates a new database or returns an existing one
                        if (m_server.DatabaseExists(graphName))
                        {
                            graphDatabase = m_server.GetDatabase(graphName);
                            success = true;
                        }
                    }
                    catch (MongoException ex)
                    {
                        if (loggingEnabled) log.Error("Could not create or retrieve database for graph with name '" + graphName + "'", ex);
                    }

                    if (success)
                    {
                        if (!graphDatabase.CollectionExists(TriplesCollectionName))
                        {
                            CommandResult result = graphDatabase.CreateCollection(TriplesCollectionName);

                            if (!result.Ok)
                            {
                                if (loggingEnabled) log.Error("Could not create triples collection for graph with name '" + graphName + "'");
                                return false;
                            }
                        }

                        success = false;

                        try
                        {
                            graphTriplesCollection = graphDatabase.GetCollection(TriplesCollectionName);
                            success = true;
                        }
                        catch (MongoException ex)
                        {
                            if (loggingEnabled) log.Error("Could not create triples collection for graph with name '" + graphName + "'", ex);
                            return false;
                        }

                        m_databaseCollectionCache[graphName] = new WeakReference(graphTriplesCollection);
                    }
                }
            }
            else
            {
                if (!collectionWeakReference.IsAlive)
                {
                    MongoDatabase mongoDatabase = m_server.GetDatabase(graphName);
                    graphTriplesCollection = mongoDatabase.GetCollection(TriplesCollectionName);
                    collectionWeakReference.Target = graphTriplesCollection;
                }
                else
                {
                    graphTriplesCollection = (MongoCollection)collectionWeakReference.Target;
                }
            }

            return true;        
        }
    }

    /// <summary>
    /// The default native mongo DB model provider
    /// Provides change notification via revision numbers
    /// 
    /// Note: In the MongoDB case, we provide a database per
    /// graph, and so the graph name maps to database name.
    /// </summary>
    public class MongoGraphStore : GraphStoreBase<ObjectId>
    {
        #region Constants
        private const string TriplesCollectionName = "Triples";
        private const string GraphNamesCollectionName = "Graphs";

        private const string GraphNamesDatabaseName = "GraphNames";
        #endregion

        private MongoServer m_server;     
        private readonly MongoDatabaseCache m_mongoDatabaseCache;

        #region Object Lifetime
        public MongoGraphStore(GraphProxy<ObjectId> graphProxy, MongoGraphStoreConnectionInfo graphStoreConnectionInfo)
            : base(graphProxy, graphStoreConnectionInfo)
        {
            try
            {
                m_server = MongoServer.Create(graphStoreConnectionInfo.ConnectionString);

                // For a MongoDB data store instance, the graph names are simply the database names
                m_mongoDatabaseCache = new MongoDatabaseCache(m_server, m_server.GetDatabaseNames());
            }
            catch (Exception ex)
            {
                throw new GraphStoreException("Could not connect to mongo database ('" + ex.Message + "'");
            }
        }

        public override void Dispose()
        {
            m_mongoDatabaseCache.Dispose();

            if (m_server != null)
            {
                m_server.Disconnect();
                m_server = null;
            }

            base.Dispose();
        }
        #endregion

        #region Change Methods       
        private MongoCollection GetMongoTriplesCollection(string graphName)
        {
            // If we are removing, the collection should exist already
            MongoCollection mongoTriplesCollection = null;
            if (!m_mongoDatabaseCache.TryGetMongoTriplesCollection(graphName, out mongoTriplesCollection))
            {
                throw new GraphStoreException("Cannot find Mongo triples collection for graph with name '" + graphName + "'");
            }

            return mongoTriplesCollection;
        }

        /// <summary>
        /// Called by the framework when removal of triples from the database is required
        /// </summary>
        protected override void SubmitRemoveBatchToStore(Graph<ObjectId> graph, IEnumerable<Triple<ObjectId>> removeChanges)
        {
            GetMongoTriplesCollection(graph.Name).Remove(Query.In("_id", new BsonArray(removeChanges.Select(t => t.ID))));
        }

        /// <summary>
        /// Called by the framework when triples in the mongo DB data store need updating
        /// </summary>
        protected override void SubmitUpdateBatchToStore(Graph<ObjectId> graph, IEnumerable<Triple<ObjectId>> updateChanges)
        {
            MongoCollection triplesCollection = GetMongoTriplesCollection(graph.Name);

            foreach (Triple<ObjectId> updateEntity in updateChanges)
            {
                triplesCollection.Save(updateEntity);
            }
        }

        /// <summary>
        /// Called by the framework when new triples need to be inserted into the MongoDB triple data store
        /// </summary>
        protected override void SubmitInsertBatchToStore(Graph<ObjectId> graph, IEnumerable<Triple<ObjectId>> insertChanges)
        {
            MongoCollection graphTriplesCollection = GetMongoTriplesCollection(graph.Name);

            foreach (Triple<ObjectId> triple in insertChanges)
            {
                graphTriplesCollection.Insert<Triple<ObjectId>>(triple);
            }
        }
        #endregion

        /// <summary>
        /// Called by the framework when an individual triple needs to be retreived from the Mongo DB data store
        /// </summary>
        protected override Triple<ObjectId> InternalLoadTripleById(string graphName, ObjectId tripleId)
        {
            MongoCollection graphTriplesCollection = GetMongoTriplesCollection(graphName);

            IMongoQuery query = Query.EQ("_id", tripleId);
            Triple<ObjectId> triple = graphTriplesCollection.FindOneAs<Triple<ObjectId>>(query);

            return triple;
        }
        
        /// <summary>
        /// Called by the framework to retrieve the named graph from the data store
        /// Returns null if not currently stored in the data store
        /// </summary>
        protected override Graph<ObjectId> InternalGetGraph(string graphName)
        {
            Graph<ObjectId> graph = null;

            try
            {
                if (m_server.DatabaseExists(graphName))
                {
                    graph = new Graph<ObjectId>(GraphProxy, graphName);
                }
            }
            catch (MongoException ex)
            {
                throw new GraphStoreException("Could not check for existence of database for graph with name '" + graphName + "'", ex);
            }

            return graph;
        }

        /// <summary>
        /// Called by the framework when a new graph needs to be registered on the MongoDB data store
        /// </summary>
        protected override void InternalRegisterGraph(Graph<ObjectId> graph)
        {
            try
            {
                if (!m_server.DatabaseExists(graph.Name))
                {
                    MongoDatabase graphDatabase = m_server.GetDatabase(graph.Name);
                    CommandResult result = graphDatabase.CreateCollection(TriplesCollectionName);

                    if (!result.Ok)
                    {
                        throw new GraphStoreException("Could not create triples collection on graph database with name '" + graph.Name + "'");
                    }
                }
            }
            catch (MongoException ex)
            {
                throw new GraphStoreException("Could not create database with name '" + graph.Name + "'", ex);
            }
        }
        
        /// <summary>
        /// Create the default query provider for querying a MongoDB database triple
        /// store using SPARQL syntax
        /// </summary>
        protected override GraphQueryProviderBase CreateDefaultQueryProvider()
        {
            return null;
        }

        /// <summary>
        /// Called by the framework when a new triple ID is required
        /// </summary>
        public override ObjectId CreateNewTripleId()
        {
            return ObjectId.GenerateNewId();
        }

        /// <summary>
        /// Returns both information about all properties (predicates) a node refers to,
        /// plus all non delay-loaded data such as value (literal) properties
        /// </summary>
        protected override IList<Tuple<string, NodePropertyInfo<ObjectId>>> InternalGetPropertyInfosForSubjectUri(Graph<ObjectId> graph, Uri subjectUri)
        {
            List<Tuple<string, NodePropertyInfo<ObjectId>>> nodePropertyInfos = new List<Tuple<string, NodePropertyInfo<ObjectId>>>();

            MongoCollection triplesCollection = null;

            if(m_mongoDatabaseCache.TryGetMongoTriplesCollection(graph.Name, out triplesCollection))
            {
                BsonValue value = new BsonString(subjectUri.ToString());                
                IMongoQuery query = Query.EQ("Subject", value);
                var cursor = triplesCollection.FindAs<Triple<ObjectId>>(query);

                foreach (Triple<ObjectId> triple in cursor)
                {
                    NodePropertyInfo<ObjectId> nodePropertyInfo = new NodePropertyInfo<ObjectId>();

                    nodePropertyInfo.ID = triple.ID;

                    // If we have a value type, get the wrapping type. If an object reference, it is a reference type
                    if (triple.IsLiteral)
                    {
                        nodePropertyInfo.ItemWrappingInfo = TypeHelper.GetItemWrappingInfo(triple.LiteralType);

                        // Literal values are not delay loaded in Ingenious Framework
                        nodePropertyInfo.LiteralValue = triple.Value;
                    }
                    else
                    {
                        // Non literal (resources) are delay-loaded in Ingenious framework
                        nodePropertyInfo.ItemWrappingInfo = new ItemWrappingInfo();
                        nodePropertyInfo.ItemWrappingInfo.ItemType = ItemType.OtherReference;                        
                    }

                    nodePropertyInfos.Add(new Tuple<string, NodePropertyInfo<ObjectId>>(triple.Predicate.ToString(), nodePropertyInfo));
                }
            }

            return nodePropertyInfos;
        }
    }
}
